{
 "cells": [
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<font color=\"red\">注</font>: 使用 tensorboard 可视化需要安装 tensorflow (TensorBoard依赖于tensorflow库，可以任意安装tensorflow的gpu/cpu版本)\n",
    "\n",
    "```shell\n",
    "pip install tensorflow-cpu\n",
    "```"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-20T02:25:46.424253Z",
     "start_time": "2025-01-20T02:25:43.589913Z"
    }
   },
   "source": [
    "import matplotlib as mpl\n",
    "import matplotlib.pyplot as plt\n",
    "%matplotlib inline\n",
    "import numpy as np\n",
    "import sklearn\n",
    "import pandas as pd\n",
    "import os\n",
    "import sys\n",
    "import time\n",
    "from tqdm.auto import tqdm\n",
    "import torch\n",
    "import torch.nn as nn\n",
    "import torch.nn.functional as F\n",
    "\n",
    "print(sys.version_info)\n",
    "for module in mpl, np, pd, sklearn, torch:\n",
    "    print(module.__name__, module.__version__)\n",
    "    \n",
    "device = torch.device(\"cuda:0\") if torch.cuda.is_available() else torch.device(\"cpu\")\n",
    "print(device)\n",
    "\n",
    "seed = 42\n"
   ],
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "sys.version_info(major=3, minor=12, micro=3, releaselevel='final', serial=0)\n",
      "matplotlib 3.10.0\n",
      "numpy 1.26.4\n",
      "pandas 2.2.3\n",
      "sklearn 1.6.0\n",
      "torch 2.5.1+cpu\n",
      "cpu\n"
     ]
    }
   ],
   "execution_count": 1
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 数据准备"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-20T02:25:47.542573Z",
     "start_time": "2025-01-20T02:25:46.425256Z"
    }
   },
   "source": [
    "from torchvision import datasets\n",
    "from torchvision.transforms import ToTensor\n",
    "from torch.utils.data import random_split\n",
    "\n",
    "# fashion_mnist图像分类数据集\n",
    "train_ds = datasets.FashionMNIST(\n",
    "    root=\"data\",\n",
    "    train=True,\n",
    "    download=True,\n",
    "    transform=ToTensor()\n",
    ")\n",
    "\n",
    "test_ds = datasets.FashionMNIST(\n",
    "    root=\"data\",\n",
    "    train=False,\n",
    "    download=True,\n",
    "    transform=ToTensor()\n",
    ")\n",
    "\n",
    "# torchvision 数据集里没有提供训练集和验证集的划分\n",
    "# 这里用 random_split 按照 11 : 1 的比例来划分数据集\n",
    "train_ds, val_ds = random_split(train_ds, [55000, 5000], torch.Generator().manual_seed(seed))"
   ],
   "outputs": [],
   "execution_count": 2
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-20T02:25:47.545878Z",
     "start_time": "2025-01-20T02:25:47.542573Z"
    }
   },
   "source": [
    "from torchvision.transforms import Normalize\n",
    "\n",
    "# 遍历train_ds得到每张图片，计算每个通道的均值和方差\n",
    "def cal_mean_std(ds):\n",
    "    mean = 0.\n",
    "    std = 0.\n",
    "    for img, _ in ds:\n",
    "        mean += img.mean(dim=(1, 2))\n",
    "        std += img.std(dim=(1, 2))\n",
    "    mean /= len(ds)\n",
    "    std /= len(ds)\n",
    "    return mean, std\n",
    "\n",
    "\n",
    "# print(cal_mean_std(train_ds))\n",
    "# 0.2860， 0.3205\n",
    "transforms = nn.Sequential(\n",
    "    Normalize([0.2856], [0.3202])\n",
    ") # 对每个通道进行标准化"
   ],
   "outputs": [],
   "execution_count": 3
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-20T02:25:47.555886Z",
     "start_time": "2025-01-20T02:25:47.545878Z"
    }
   },
   "source": [
    "from torch.utils.data.dataloader import DataLoader\n",
    "\n",
    "batch_size = 32\n",
    "# 从数据集到dataloader\n",
    "train_loader = DataLoader(train_ds, batch_size=batch_size, shuffle=True, num_workers=4)\n",
    "val_loader = DataLoader(val_ds, batch_size=batch_size, shuffle=False, num_workers=4)\n",
    "test_loader = DataLoader(test_ds, batch_size=batch_size, shuffle=False, num_workers=4)"
   ],
   "outputs": [],
   "execution_count": 4
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 定义模型"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-20T02:25:47.568904Z",
     "start_time": "2025-01-20T02:25:47.556892Z"
    }
   },
   "source": [
    "\n",
    "class CNN(nn.Module):\n",
    "    def __init__(self, activation=\"relu\"):\n",
    "        super(CNN, self).__init__()\n",
    "        self.activation = F.relu if activation == \"relu\" else F.selu\n",
    "        self.conv1 = nn.Conv2d(in_channels=1, out_channels=32, kernel_size=3, padding=1)\n",
    "        self.conv2 = nn.Conv2d(in_channels=32, out_channels=32, kernel_size=3, padding=1)\n",
    "        self.pool = nn.MaxPool2d(2, 2)\n",
    "        self.conv3 = nn.Conv2d(in_channels=32, out_channels=64, kernel_size=3, padding=1)\n",
    "        self.conv4 = nn.Conv2d(in_channels=64, out_channels=64, kernel_size=3, padding=1)\n",
    "        self.conv5 = nn.Conv2d(in_channels=64, out_channels=128, kernel_size=3, padding=1)\n",
    "        self.conv6 = nn.Conv2d(in_channels=128, out_channels=128, kernel_size=3, padding=1)\n",
    "        self.flatten = nn.Flatten()\n",
    "        # input shape is (28, 28, 1) so the fc1 layer in_features is 128 * 3 * 3\n",
    "        self.fc1 = nn.Linear(128 * 3 * 3, 128)\n",
    "        self.fc2 = nn.Linear(128, 10)\n",
    "        \n",
    "        self.init_weights()\n",
    "        \n",
    "    def init_weights(self):\n",
    "        \"\"\"使用 xavier 均匀分布来初始化全连接层、卷积层的权重 W\"\"\"\n",
    "        for m in self.modules():\n",
    "            if isinstance(m, (nn.Linear, nn.Conv2d)):\n",
    "                nn.init.xavier_uniform_(m.weight)\n",
    "                nn.init.zeros_(m.bias)\n",
    "        \n",
    "    def forward(self, x):\n",
    "        act = self.activation\n",
    "        x = self.pool(act(self.conv2(act(self.conv1(x)))))\n",
    "        x = self.pool(act(self.conv4(act(self.conv3(x)))))\n",
    "        x = self.pool(act(self.conv6(act(self.conv5(x)))))\n",
    "        x = self.flatten(x)\n",
    "        x = act(self.fc1(x))\n",
    "        x = self.fc2(x)\n",
    "        return x\n",
    "    \n",
    "\n",
    "for idx, (key, value) in enumerate(CNN().named_parameters()):\n",
    "    print(f\"{key}\\tparamerters num: {np.prod(value.shape)}\")\n"
   ],
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "conv1.weight\tparamerters num: 288\n",
      "conv1.bias\tparamerters num: 32\n",
      "conv2.weight\tparamerters num: 9216\n",
      "conv2.bias\tparamerters num: 32\n",
      "conv3.weight\tparamerters num: 18432\n",
      "conv3.bias\tparamerters num: 64\n",
      "conv4.weight\tparamerters num: 36864\n",
      "conv4.bias\tparamerters num: 64\n",
      "conv5.weight\tparamerters num: 73728\n",
      "conv5.bias\tparamerters num: 128\n",
      "conv6.weight\tparamerters num: 147456\n",
      "conv6.bias\tparamerters num: 128\n",
      "fc1.weight\tparamerters num: 147456\n",
      "fc1.bias\tparamerters num: 128\n",
      "fc2.weight\tparamerters num: 1280\n",
      "fc2.bias\tparamerters num: 10\n"
     ]
    }
   ],
   "execution_count": 5
  },
  {
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-20T06:19:42.348627Z",
     "start_time": "2025-01-20T06:19:42.285529Z"
    }
   },
   "cell_type": "code",
   "source": [
    "def count_parameters(model): #计算模型总参数量\n",
    "    return sum(p.numel() for p in model.parameters() if p.requires_grad)\n",
    "count_parameters(CNN())"
   ],
   "outputs": [
    {
     "data": {
      "text/plain": [
       "435306"
      ]
     },
     "execution_count": 10,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "execution_count": 10
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 训练\n",
    "\n",
    "pytorch的训练需要自行实现，包括\n",
    "1. 定义损失函数\n",
    "2. 定义优化器\n",
    "3. 定义训练步\n",
    "4. 训练"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-20T02:25:47.607932Z",
     "start_time": "2025-01-20T02:25:47.568904Z"
    }
   },
   "source": [
    "from sklearn.metrics import accuracy_score\n",
    "\n",
    "@torch.no_grad()\n",
    "def evaluating(model, dataloader, loss_fct):\n",
    "    loss_list = []\n",
    "    pred_list = []\n",
    "    label_list = []\n",
    "    for datas, labels in dataloader:\n",
    "        datas = datas.to(device)\n",
    "        labels = labels.to(device)\n",
    "        # 前向计算\n",
    "        logits = model(datas)\n",
    "        loss = loss_fct(logits, labels)         # 验证集损失\n",
    "        loss_list.append(loss.item())\n",
    "        \n",
    "        preds = logits.argmax(axis=-1)    # 验证集预测\n",
    "        pred_list.extend(preds.cpu().numpy().tolist())\n",
    "        label_list.extend(labels.cpu().numpy().tolist())\n",
    "        \n",
    "    acc = accuracy_score(label_list, pred_list)\n",
    "    return np.mean(loss_list), acc\n"
   ],
   "outputs": [],
   "execution_count": 6
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### TensorBoard 可视化\n",
    "\n",
    "\n",
    "训练过程中可以使用如下命令启动tensorboard服务。\n",
    "\n",
    "```shell\n",
    "tensorboard \\\n",
    "    --logdir=runs \\     # log 存放路径\n",
    "    --host 0.0.0.0 \\    # ip\n",
    "    --port 8848         # 端口\n",
    "```"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-20T02:25:50.192289Z",
     "start_time": "2025-01-20T02:25:47.607932Z"
    }
   },
   "source": [
    "from torch.utils.tensorboard import SummaryWriter\n",
    "\n",
    "\n",
    "class TensorBoardCallback:\n",
    "    def __init__(self, log_dir, flush_secs=10):\n",
    "        \"\"\"\n",
    "        Args:\n",
    "            log_dir (str): dir to write log.\n",
    "            flush_secs (int, optional): write to dsk each flush_secs seconds. Defaults to 10.\n",
    "        \"\"\"\n",
    "        self.writer = SummaryWriter(log_dir=log_dir, flush_secs=flush_secs)\n",
    "\n",
    "    def draw_model(self, model, input_shape):\n",
    "        self.writer.add_graph(model, input_to_model=torch.randn(input_shape))\n",
    "        \n",
    "    def add_loss_scalars(self, step, loss, val_loss):\n",
    "        self.writer.add_scalars(\n",
    "            main_tag=\"training/loss\", \n",
    "            tag_scalar_dict={\"loss\": loss, \"val_loss\": val_loss},\n",
    "            global_step=step,\n",
    "            )\n",
    "        \n",
    "    def add_acc_scalars(self, step, acc, val_acc):\n",
    "        self.writer.add_scalars(\n",
    "            main_tag=\"training/accuracy\",\n",
    "            tag_scalar_dict={\"accuracy\": acc, \"val_accuracy\": val_acc},\n",
    "            global_step=step,\n",
    "        )\n",
    "        \n",
    "    def add_lr_scalars(self, step, learning_rate):\n",
    "        self.writer.add_scalars(\n",
    "            main_tag=\"training/learning_rate\",\n",
    "            tag_scalar_dict={\"learning_rate\": learning_rate},\n",
    "            global_step=step,\n",
    "            \n",
    "        )\n",
    "    \n",
    "    def __call__(self, step, **kwargs):\n",
    "        # add loss\n",
    "        loss = kwargs.pop(\"loss\", None)\n",
    "        val_loss = kwargs.pop(\"val_loss\", None)\n",
    "        if loss is not None and val_loss is not None:\n",
    "            self.add_loss_scalars(step, loss, val_loss)\n",
    "        # add acc\n",
    "        acc = kwargs.pop(\"acc\", None)\n",
    "        val_acc = kwargs.pop(\"val_acc\", None)\n",
    "        if acc is not None and val_acc is not None:\n",
    "            self.add_acc_scalars(step, acc, val_acc)\n",
    "        # add lr\n",
    "        learning_rate = kwargs.pop(\"lr\", None)\n",
    "        if learning_rate is not None:\n",
    "            self.add_lr_scalars(step, learning_rate)\n"
   ],
   "outputs": [],
   "execution_count": 7
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Save Best\n"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-20T02:25:50.196338Z",
     "start_time": "2025-01-20T02:25:50.192289Z"
    }
   },
   "source": [
    "class SaveCheckpointsCallback:\n",
    "    def __init__(self, save_dir, save_step=5000, save_best_only=True):\n",
    "        \"\"\"\n",
    "        Save checkpoints each save_epoch epoch. \n",
    "        We save checkpoint by epoch in this implementation.\n",
    "        Usually, training scripts with pytorch evaluating model and save checkpoint by step.\n",
    "\n",
    "        Args:\n",
    "            save_dir (str): dir to save checkpoint\n",
    "            save_epoch (int, optional): the frequency to save checkpoint. Defaults to 1.\n",
    "            save_best_only (bool, optional): If True, only save the best model or save each model at every epoch.\n",
    "        \"\"\"\n",
    "        self.save_dir = save_dir\n",
    "        self.save_step = save_step\n",
    "        self.save_best_only = save_best_only\n",
    "        self.best_metrics = -1\n",
    "        \n",
    "        # mkdir\n",
    "        if not os.path.exists(self.save_dir):\n",
    "            os.mkdir(self.save_dir)\n",
    "        \n",
    "    def __call__(self, step, state_dict, metric=None):\n",
    "        if step % self.save_step > 0:\n",
    "            return\n",
    "        \n",
    "        if self.save_best_only:\n",
    "            assert metric is not None\n",
    "            if metric >= self.best_metrics:\n",
    "                # save checkpoints\n",
    "                torch.save(state_dict, os.path.join(self.save_dir, \"best.ckpt\"))\n",
    "                # update best metrics\n",
    "                self.best_metrics = metric\n",
    "        else:\n",
    "            torch.save(state_dict, os.path.join(self.save_dir, f\"{step}.ckpt\"))\n",
    "\n"
   ],
   "outputs": [],
   "execution_count": 8
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Early Stop"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-20T02:26:31.405893Z",
     "start_time": "2025-01-20T02:26:31.402169Z"
    }
   },
   "source": [
    "class EarlyStopCallback:\n",
    "    def __init__(self, patience=5, min_delta=0.01):\n",
    "        \"\"\"\n",
    "\n",
    "        Args:\n",
    "            patience (int, optional): Number of epochs with no improvement after which training will be stopped.. Defaults to 5.\n",
    "            min_delta (float, optional): Minimum change in the monitored quantity to qualify as an improvement, i.e. an absolute \n",
    "                change of less than min_delta, will count as no improvement. Defaults to 0.01.\n",
    "        \"\"\"\n",
    "        self.patience = patience\n",
    "        self.min_delta = min_delta\n",
    "        self.best_metric = -1\n",
    "        self.counter = 0\n",
    "        \n",
    "    def __call__(self, metric):\n",
    "        if metric >= self.best_metric + self.min_delta:\n",
    "            # update best metric\n",
    "            self.best_metric = metric\n",
    "            # reset counter \n",
    "            self.counter = 0\n",
    "        else: \n",
    "            self.counter += 1\n",
    "            \n",
    "    @property\n",
    "    def early_stop(self):\n",
    "        return self.counter >= self.patience\n"
   ],
   "outputs": [],
   "execution_count": 9
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2023-12-04T03:36:33.168264300Z",
     "start_time": "2023-12-04T03:08:27.257198Z"
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": "  0%|          | 0/34380 [00:00<?, ?it/s]",
      "application/vnd.jupyter.widget-view+json": {
       "version_major": 2,
       "version_minor": 0,
       "model_id": "80122be696cf481d859c0bbc6089d861"
      }
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Early stop at epoch 12 / global_step 22000\n"
     ]
    }
   ],
   "source": [
    "# 训练\n",
    "def training(\n",
    "    model, \n",
    "    train_loader, \n",
    "    val_loader, \n",
    "    epoch, \n",
    "    loss_fct, \n",
    "    optimizer, \n",
    "    tensorboard_callback=None,\n",
    "    save_ckpt_callback=None,\n",
    "    early_stop_callback=None,\n",
    "    eval_step=500,\n",
    "    ):\n",
    "    record_dict = {\n",
    "        \"train\": [],\n",
    "        \"val\": []\n",
    "    }\n",
    "    \n",
    "    global_step = 0\n",
    "    model.train()\n",
    "    with tqdm(total=epoch * len(train_loader)) as pbar:\n",
    "        for epoch_id in range(epoch):\n",
    "            # training\n",
    "            for datas, labels in train_loader:\n",
    "                datas = datas.to(device)\n",
    "                labels = labels.to(device)\n",
    "                # 梯度清空\n",
    "                optimizer.zero_grad()\n",
    "                # 模型前向计算\n",
    "                logits = model(datas)\n",
    "                # 计算损失\n",
    "                loss = loss_fct(logits, labels)\n",
    "                # 梯度回传\n",
    "                loss.backward()\n",
    "                # 调整优化器，包括学习率的变动等\n",
    "                optimizer.step()\n",
    "                preds = logits.argmax(axis=-1)\n",
    "            \n",
    "                acc = accuracy_score(labels.cpu().numpy(), preds.cpu().numpy())    \n",
    "                loss = loss.cpu().item()\n",
    "                # record\n",
    "                \n",
    "                record_dict[\"train\"].append({\n",
    "                    \"loss\": loss, \"acc\": acc, \"step\": global_step\n",
    "                })\n",
    "                \n",
    "                # evaluating\n",
    "                if global_step % eval_step == 0:\n",
    "                    model.eval()\n",
    "                    val_loss, val_acc = evaluating(model, val_loader, loss_fct)\n",
    "                    record_dict[\"val\"].append({\n",
    "                        \"loss\": val_loss, \"acc\": val_acc, \"step\": global_step\n",
    "                    })\n",
    "                    model.train()\n",
    "                    \n",
    "                    # 1. 使用 tensorboard 可视化\n",
    "                    if tensorboard_callback is not None:\n",
    "                        tensorboard_callback(\n",
    "                            global_step, \n",
    "                            loss=loss, val_loss=val_loss,\n",
    "                            acc=acc, val_acc=val_acc,\n",
    "                            lr=optimizer.param_groups[0][\"lr\"],\n",
    "                            )\n",
    "                    \n",
    "                    # 2. 保存模型权重 save model checkpoint\n",
    "                    if save_ckpt_callback is not None:\n",
    "                        save_ckpt_callback(global_step, model.state_dict(), metric=val_acc)\n",
    "\n",
    "                    # 3. 早停 Early Stop\n",
    "                    if early_stop_callback is not None:\n",
    "                        early_stop_callback(val_acc)\n",
    "                        if early_stop_callback.early_stop:\n",
    "                            print(f\"Early stop at epoch {epoch_id} / global_step {global_step}\")\n",
    "                            return record_dict\n",
    "                    \n",
    "                # udate step\n",
    "                global_step += 1\n",
    "                pbar.update(1)\n",
    "                pbar.set_postfix({\"epoch\": epoch_id})\n",
    "        \n",
    "    return record_dict\n",
    "        \n",
    "\n",
    "epoch = 20\n",
    "\n",
    "activation = \"selu\"\n",
    "model = CNN(activation)\n",
    "\n",
    "# 1. 定义损失函数 采用交叉熵损失\n",
    "loss_fct = nn.CrossEntropyLoss()\n",
    "# 2. 定义优化器 采用SGD\n",
    "# Optimizers specified in the torch.optim package\n",
    "optimizer = torch.optim.SGD(model.parameters(), lr=0.001, momentum=0.9)\n",
    "\n",
    "# 1. tensorboard 可视化\n",
    "if not os.path.exists(\"runs\"):\n",
    "    os.mkdir(\"runs\")\n",
    "tensorboard_callback = TensorBoardCallback(f\"runs/cnn-{activation}\")\n",
    "tensorboard_callback.draw_model(model, [1, 1, 28, 28])\n",
    "# 2. save best\n",
    "if not os.path.exists(\"checkpoints\"):\n",
    "    os.makedirs(\"checkpoints\")\n",
    "save_ckpt_callback = SaveCheckpointsCallback(f\"checkpoints/cnn-{activation}\", save_best_only=True)\n",
    "# 3. early stop\n",
    "early_stop_callback = EarlyStopCallback(patience=10)\n",
    "\n",
    "model = model.to(device)\n",
    "record = training(\n",
    "    model, \n",
    "    train_loader, \n",
    "    val_loader, \n",
    "    epoch, \n",
    "    loss_fct, \n",
    "    optimizer, \n",
    "    tensorboard_callback=None,\n",
    "    save_ckpt_callback=save_ckpt_callback,\n",
    "    early_stop_callback=early_stop_callback,\n",
    "    eval_step=1000\n",
    "    )"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2023-12-04T03:36:33.438329800Z",
     "start_time": "2023-12-04T03:36:33.189268800Z"
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": "<Figure size 720x360 with 2 Axes>",
      "image/png": "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\n"
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    }
   ],
   "source": [
    "#画线要注意的是损失是不一定在零到1之间的\n",
    "def plot_learning_curves(record_dict, sample_step=500):\n",
    "    # build DataFrame\n",
    "    train_df = pd.DataFrame(record_dict[\"train\"]).set_index(\"step\").iloc[::sample_step]\n",
    "    val_df = pd.DataFrame(record_dict[\"val\"]).set_index(\"step\")\n",
    "\n",
    "    # plot\n",
    "    fig_num = len(train_df.columns)\n",
    "    fig, axs = plt.subplots(1, fig_num, figsize=(5 * fig_num, 5))\n",
    "    for idx, item in enumerate(train_df.columns):    \n",
    "        axs[idx].plot(train_df.index, train_df[item], label=f\"train_{item}\")\n",
    "        axs[idx].plot(val_df.index, val_df[item], label=f\"val_{item}\")\n",
    "        axs[idx].grid()\n",
    "        axs[idx].legend()\n",
    "        axs[idx].set_xticks(range(0, train_df.index[-1], 5000))\n",
    "        axs[idx].set_xticklabels(map(lambda x: f\"{int(x/1000)}k\", range(0, train_df.index[-1], 5000)))\n",
    "        axs[idx].set_xlabel(\"step\")\n",
    "    \n",
    "    plt.show()\n",
    "\n",
    "plot_learning_curves(record, sample_step=500)  #横坐标是 steps"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 评估"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2023-12-04T03:36:44.824242200Z",
     "start_time": "2023-12-04T03:36:33.440329600Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "loss:     0.2931\n",
      "accuracy: 0.9117\n"
     ]
    }
   ],
   "source": [
    "# dataload for evaluating\n",
    "\n",
    "# load checkpoints\n",
    "model.load_state_dict(torch.load(f\"checkpoints/cnn-{activation}/best.ckpt\", map_location=\"cpu\"))\n",
    "\n",
    "model.eval()\n",
    "loss, acc = evaluating(model, test_loader, loss_fct)\n",
    "print(f\"loss:     {loss:.4f}\\naccuracy: {acc:.4f}\")"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "pytorch",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.10.8"
  },
  "orig_nbformat": 4
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
